<?php
require_once("CacheTagSettings.class.php");

/**
 * CacheTag class - library enhansing any cache with tags.
 *
 * @package phpCacheTag
 * @author Umkus
 * @version 0.1.5
 */
abstract class CacheTag extends CacheTagSettings
{
	/**************************************
	*            Properties              *
	**************************************/


 /**
  * Objects of this class implementation
  * @var array
  */
	private static $instances = array();

 /**
  * Backend engine string
  * @var string CONST
  */
	private static $backend = '';

 /**
  * Tags array names
  * @var array
  */
	private $tags = array();

 /**
  * Function/method name to call
  * @var <type>
  */
	private $function = array();

 /**
  * Extra parameters to distinct one function call from another
  * @var array
  */
	private $params = array();

 /**
  * Current cache item variable name
  * @var string
  *
  * @todo this variable unused
  */
	private $varName = "";

 /**
  * Variable-specific time-out value
  * @var boolean
  */
	private $customTimeout = false;

 /**
  * Construct -> read this param, on destruct -> write
  * @var array
  */
	private $globalResetTags = array();

 /**
  * Set action
  * @var string CONST ACTION_*
  */
	private $action = '';

	const ACTION_FLUSH = 'flush';
	const ACTION_FETCH = 'fetch';



	/**************************************
	*            Abstraction             *
	**************************************/

  /**
   * Returns value of the variable in cache
   *
   * @abstract
   * @param string $name
   * @return	mixed
   */
	abstract protected function Get($name);

  /**
   * Deletes variable in cache
   *
   * @abstract
   * @param	string	$name
   * @return	boolean
   */
	abstract protected function Delete($name);

  /**
   * Adds/replaces variable in cache
   *
   * @abstract
   * @param	string	$varName
   * @param	mixed	$value
   * @param	int		$timeoutMinutes 	minutes to store data
   * @return	mixed
   */
	abstract protected function Set($varName, $value, $timeout = 0);

 /**
  * Replaces a value of the variable by name
  *
  * @abstract
  * @param string $name
  * @param mixed $value
  * @return	mixed
  */
  abstract protected function Replace($name, $value);


	/**************************************
	*                Magic               *
	**************************************/


	protected function __construct()
	{
		//Disable cache?
		self::$enabled = !isset($_GET[$this->disableCacheGetKey]) && self::$enabled == true;

		//Don't disable cache, just replace everything in it
		$this->replace = isset($_GET[$this->resetCacheGetKey]);

		$this->debug && print 'Getting '.$this->tagsVarName.'<br />';
		//Getting main tag variable. Everything is working through it.
		$this->globalResetTags = $this->Get($this->tagsVarName);

		//Ya, this has to an array
		if (!is_array($this->globalResetTags))
		{
			$this->globalResetTags = array();
		}
	}

	public function __destruct()
	{
		//We're done. Save tags array to cache.
		$this->SaveResetTags($this->globalResetTags);

		return true;
	}



	/**************************************
	*          Private methods           *
	**************************************/


	/**
	 * Includes current backend class
	 *
	 * @return	boolean
	 */
	private static function ImportBackend()
	{
		$className = self::$backend.'Tag';

		$dirName = pathinfo(__FILE__);
		$dirName = $dirName['dirname'];

		$classFile =  $dirName . '/' . $className.'.class.php';

		if (!file_exists($classFile))
		{
			throw new Exception('Class file for '.self::$backend.' not found');
		}

		require_once($classFile);

		if (!class_exists($className))
		{
			throw new Exception('Class '.$className.' not found in '.$classFile);
		}

		return true;
	}

	/**
	 * Returns the object. Singletone stuff.
	 *
	 * @return	object
	 */
	private static function GetInstance()
	{
		if (!isset(self::$instances[self::$backend]))
		{
			$className = self::$backend.'Tag';

			if (!class_exists($className))
			{
				self::ImportBackend();
			}

			self::$instances[self::$backend] = new $className;

		}

		return self::$instances[self::$backend];
	}

	/**
	 * Get the result of function execution - array containing function return value and any output there was in process
	 *
	 * @return	array
	 */
	private function GetFunctionValue()
	{
		$function = (array)$this->function;
		$funcName = array_shift($function);

		//Getting whatever function/method may print out in the process
		ob_start();
		$value = call_user_func_array($funcName, $function);
		$output = ob_get_clean();

		return array($value, $output);
	}

	/**
	 * Generates variable name, based on the function/method call and its params
	 *
	 * @return	string
	 */
	protected function GetVarName()
	{
		//Taking just the first tag. Varaiables under other tags just would have been this var's duplicates.

		//http_build_query() is just the easyest way to join a key=>value array

		//Variable names must always have parameters.

		$params = http_build_query($this->params);

		if (!empty($params))
		{
			$params = '?'.$params;
		}

		return $this->tags[0] . $params;
	}

	/**
	 * Resets settings to current session defaults before another cache fetch
	 *
	 * @return boolean
	 */
	private function ResetSettings()
	{
		$this->replace = isset($_GET[$this->resetCacheGetKey]);
		$this->tags = array();
		$this->params = array();
		$this->function = "";
		$this->customTimeout = false;
		$this->updateGlobalResetTags = false;

		return true;
	}

	/**
	 * Prepares tags and params arrays for usage
	 *
	 * @return	boolean
	 */
	private function Prepare()
	{
		//Sorting is needed not to duplicate variables with same parans but different order
		ksort($this->params);
		$this->params = array_unique($this->params);

		sort($this->tags);
		$this->tags = array_unique($this->tags);

		return true;
	}

	/**
	 * Returns true if this variable is found in some of the tags
	 *
	 * @param 	string	$varName
	 * @return	boolean
	 */
	private function IsRegistered()
	{
		//Check if all of current tags exist in tags array

		//Some tag(s) is missing. Cache is no good.
		if (array_diff_key($this->globalResetTags, array_combine($this->tags, $this->tags)) !== array())
		{
			return false;
		}
		//Ok, all tags exist.

		//Now check if all of current params are set in tags array. Within every tag, yes.
		foreach ($this->tags as $currentTag)
		{
			//Current tag(s) doesn't exist?
			if (!isset($this->globalResetTags[$currentTag]))
			{
				return false;
			}
			//One or more of current tag's params does not exists in this tag
			elseif(array_diff_key($this->params, $this->globalResetTags[$currentTag]) !== array())
			{
				return false;
			}

			//Ok all params exist in this tag

			//Check if this param holds our variable
			foreach ($this->params as $currentParam => $currentParamValue)
			{
				//Param value not found in this tag's param array. Cache is no good.
				if (!in_array($currentParamValue, $this->globalResetTags[$currentTag][$currentParam]))
				{
					return false;
				}
			}
		}

		//Since we got this far, everything must be in place.
		return true;
	}

	/**
	 * Modifies tags array base on current changes
	 *
	 * @return	boolean
	 */
	private function ApplyTagsChange()
	{
		//No params specified. Tag = cache variable
		if (empty($this->params))
		{
			//Deleting whole tag(s)
			if ($this->action == self::ACTION_FLUSH)
			{
				//Delete current tags from global tags array
				$this->globalResetTags = array_diff_key($this->globalResetTags, array_combine($this->tags, $this->tags));
			}

			//Adding value identified just with tag(s), which is pretty dumb, if you ask me. Variable is identified just with a tag name, so tag IS a variable name.
			//Just add tag name to the tags array to note that this tag exist
			else
			{
				$currentTag = reset($this->tags);

				//Don't have this tag yet?
				if (!isset($this->globalResetTags[$currentTag]))
				{
					//Add it
					$this->globalResetTags[$currentTag] = array();
				}
			}
		}
		//Got some params
		else
		{
			//For each tag
			foreach ($this->tags as $currentTag)
			{
				//Don't have this tag yet?
				if (!isset($this->globalResetTags[$currentTag]))
				{
					//Add it
					$this->globalResetTags[$currentTag] = array();
				}

				//For each param in this tag
				foreach ($this->params as $currentParamName=>$currentParamValue)
				{
					//Parameter doesn't yet exist?
					if (!isset($this->globalResetTags[$currentTag][$currentParamName]))
					{
						//Initialize it with current value.
						$this->globalResetTags[$currentTag][$currentParamName] = array($currentParamValue);
					}

					//Deleting?
					if ($this->action == self::ACTION_FLUSH)
					{
						$this->globalResetTags[$currentTag][$currentParamName] = array_diff($this->globalResetTags[$currentTag][$currentParamName], array($currentParamValue));
					}
					//Adding?
					elseif (false === array_search($currentParamValue, $this->globalResetTags[$currentTag][$currentParamName]))
					{
						$this->globalResetTags[$currentTag][$currentParamName][] = $currentParamValue;
					}
				}
			}
		}

		return true;
	}

	/**
	 * Saves/replaces array tags array into cache
	 *
	 * @return	boolean
	 */
	private function SaveResetTags()
	{
		$this->debug && print 'Replacing '.$this->tagsVarName.'<br />';
		$res = $this->Replace($this->tagsVarName, $this->globalResetTags);

		if (!$res)
		{
			$this->debug && print 'Setting '.$this->tagsVarName.'<br />';
			$this->Set($this->tagsVarName, $this->globalResetTags, 0);
		}

		return true;
	}



	/**************************************
	*           Public methods           *
	**************************************/


	/**
	 * Sets the current caching backend
	 *
	 * @param	string	$backend
	 * @return	boolean
	 */
	public static function SetBackend($backend)
	{
		self::$backend = $backend;
		return true;
	}


	/**
	 * Return cache value creating it if needed (this is the main method)
	 *
   * @example example.php
	 * @param	mixed		$varName
	 * @return 	mixed
	 */
	public static function Fetch()
	{
		$self = self::GetInstance();

		$self->action = self::ACTION_FETCH;

		//If caching not enabled, then just return function result
		if (!self::$enabled)
		{
			list($value, $output) = $self->GetFunctionValue();
			!empty($output) && print $output;

			return $value;
		}

		//We need at least a tag to do something on
		if (empty($self->tags) && !isset($self->function))
		{
			return null;
		}

		$self->Prepare();

		$varName = $self->GetVarName();

		//Must be in cache
		if ($self->IsRegistered() && !$self->replace)
		{
			$self->debug && print 'Getting '.$varName.'<br />';
			//Trying to get a value from cache.
			$value = $self->Get($varName);

			//Not found in cache? Holy crap! Add it!
			if (!is_array($value))
			{
				list($value, $output) = $self->GetFunctionValue();
				$self->ApplyTagsChange();

				$self->debug && print 'Setting '.$varName.'<br />';
				$self->Set($varName, array($value, $output), $self->customTimeout !== false ? (float)$self->customTimeout : $self->timeout);
			}
			//Ok, found
			else
			{
				list($value, $output) = $value;
			}
		}
		//Not registered. Or must regenerate. Add!
		else
		{
			list($value, $output) = $self->GetFunctionValue();
			$self->ApplyTagsChange();

			$self->debug && print 'Setting '.$varName.'<br />';
			$self->Set($varName, array($value, $output), $self->customTimeout !== false ? (float)$self->customTimeout : $self->timeout);
		}

		$self->ResetSettings();

		!empty($output) && print $output;

		return $value;
	}

	/**
	 * Sets extra params to distinct value in cache
	 *
	 * @param	mixed	$param
	 * @param	mixed	$value
	 * @return	boolean
	 */
	public static function SetParam($param, $value)
	{
		$self = self::GetInstance();
		$self->params[$param] = $value;
		return true;
	}

	/**
	 * Deletes selected items from cache
	 *
	 * @return	boolean
	 */
	public static function Flush()
	{
		$self = self::GetInstance();

		$self->Prepare();

		$self->action = self::ACTION_FLUSH;

		if (empty($self->tags))
		{
			return false;
		}

		if (!$self->dontDelete)
		{
			$self->debug && print 'Deleting '.$self->GetVarName().'<br />';
			$self->Delete($self->GetVarName());
		}

		$self->ApplyTagsChange();

		$self->ResetSettings();

		return true;
	}

	/**
	 * Add tag for the current value
	 *
	 * @param	constant	$tag
	 * @return	boolean
	 */
	public static function SetTags($tags)
	{
		if (!self::$enabled)
		{
			return false;
		}

		$self = self::GetInstance();
		$self->tags = func_get_args();

		return true;
	}

	/**
	 * Makes forced cache reset for current value
	 * @return boolean
	 */
	public function SetReplace()
	{
		if (!self::$enabled)
		{
			return false;
		}

		$self = self::GetInstance();
		$self->replace = true;

		return true;
	}

	/**
	 * Sets function/method where get data from
	 * @param	mixed		$params
	 * @return	boolean
	 *
	 */
	public static function SetFunction($params)
	{
		$self = self::GetInstance();
		$self->function = func_get_args();

		return true;
	}

	/**
	 * Sets custom timeout for variable
	 *
	 * @param	integer		$minutes
	 * @return	boolean
	 */
	public function SetTimeout($minutes)
	{
		$self = self::GetInstance();
		$self->customTimeout = (float)$minutes;

		return true;
	}
}